<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/ol.min.css"
    integrity="sha512-bc9nJM5uKHN+wK7rtqMnzlGicwJBWR11SIDFJlYBe5fVOwjHGtXX8KMyYZ4sMgSL0CoUjo4GYgIBucOtqX/RUQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer" />
  <title>点位聚合</title>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --top-height: 50px;
    }

    html,
    body,
    #app,
    .app-map {
      height: 100%;
      height: 100%;
    }

    .app-btns {
      position: fixed;
      right: 10px;
      top: 10px;
      background-color: #fff;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .5);
      width: 210px;
      padding: 25px;
      text-align: center;
      border-radius: 5px;
      display: flex;
      flex-direction: column;
      z-index: 2;
    }

    .app-btns button {
      font-size: 18px;
      border: none;
      padding: 12px 10px;
      border-radius: 4px;
      color: #fff;
      background-color: #409eff;
      border-color: #409eff;
      cursor: pointer;
      border: 1px solid #dcdfe6;
      margin-bottom: 5px;
    }

    .app-btns button:hover {
      background: #66b1ff;
      border-color: #66b1ff;
    }

    .app-btns button.active {
      background-color: #07c160;
    }

    hr {
      margin: 20px 0;
    }

    .zoom {
      position: fixed;
      right: 10px;
      bottom: 10px;
      z-index: 10;
      color: #20b1aa;
      font-weight: bold;
    }
  </style>

</head>

<body>
  <div id="app">
    <div class="app-map" id="app-map"></div>
    <span v-text='mapZoom' class="zoom"></span>
    <div class="app-btns">
      <button type="button" @click='handleClickCluster(btn)' v-for='btn in btnData' v-text='btn.text'
        :class='{active: currentDis === btn.px}' :key='btn.px'></button>
      <hr>
      <button type="button" :class='[{active: cancalClusterType === 1}, "hide-type"]'
        @click='handleClickCancalCluster(1)'>样式区别 不聚合</button>
      <button type="button" :class='[{active: cancalClusterType === 2}, "hide-type"]'
        @click='handleClickCancalCluster(2)'>隐藏显示 不聚合</button>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/dist/ol.min.js"
    integrity="sha512-+nvfloZUX7awRy1yslYBsicmHKh/qFW5w79+AiGiNcbewg0nBy7AS4G3+aK/Rm+eGPOKlO3tLuVphMxFXeKeOQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.14/vue.global.prod.min.js"
    integrity="sha512-huEQFMCpBzGkSDSPVAeQFMfvWuQJWs09DslYxQ1xHeaCGQlBiky9KKZuXX7zfb0ytmgvfpTIKKAmlCZT94TAlQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>

  <script>
    const { createApp } = Vue;
    // feature图片
    const base64Img = '';
    // 基础样式
    const basePointStyle = new ol.style.Style({
      image: new ol.style.Icon({
        src: base64Img,
        scale: 1,
        anchor: [0.5, 0.5],
        rotateWithView: true,
        rotation: 0,
        opacity: 1
      }),
      count: 1
    });
    // 生成点位聚合显示的数字样式 
    const createCountPointStyle = (size) => {
      // 计算一个动态的 radius
      const radius = 20 + Math.max(0, (String(size).length - 2)) * 10;
      const rcolor = '#' + parseInt(Math.random() * 0xffffff).toString(16).padStart(6, '0');
      return new ol.style.Style({
        image: new ol.style.Circle({
          radius,
          stroke: new ol.style.Stroke({
            color: rcolor
          }),
          fill: new ol.style.Fill({
            color: rcolor
          })
        }),
        text: new ol.style.Text({
          text: size.toString(),
          fill: new ol.style.Fill({
            color: '#fff'
          }),
          scale: 2,
          textBaseline: 'middle'
        })
      })
    };
    // 存放各个聚合数字的样式，用于不重复生成各个数量的样式
    const countStyles = {};
    const vm = createApp({
      data() {
        return {
          map: {},
          mapZoom: 5,
          btnData: [
            {
              text: '50px聚合',
              px: 50,
            },
            {
              text: '100px聚合',
              px: 100,
            },
            {
              text: '150px聚合',
              px: 150,
            }
          ],
          currentDis: 150, // 聚合距离
          pointsLayers: null, // 不聚合的点位图层
          allPoints: [], // 全国范围内点位聚合
          bjPoints: [], // 北京点位聚合
          allCluster: null, // 随机的聚合图层
          beijingCluster: null, // 北京的聚合图层
          cancalClusterType: 0 // 取消聚合的方式，1 采用两种聚合样式的方式， 2 采用隐藏和显示图层的方式
        }
      },
      methods: {
        // 初始化地图
        initMap() {
          // 高德地图瓦片地址
          const vectorLayer = new ol.layer.Tile({
            source: new ol.source.XYZ({
              url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
            }),
            layerID: 'base'
          });
          //  初始化地图
          this.map = new ol.Map({
            target: 'app-map',
            layers: [vectorLayer],
            view: new ol.View({
              projection: 'EPSG:3857',
              // 设定中心点，因为默认坐标系为 3587，所以要将我们常用的经纬度坐标系4326 转换为 3587坐标系
              center: ol.proj.transform([111.8453154, 32.7383500], 'EPSG:4326', 'EPSG:3857'),
              zoom: 5,
            })
          });
          // 绑定地图事件
          this.bindMapEvt();
          // 创建随机点位的聚合图层
          this.createRandomCluster(10000);
          // 创建北京的聚合图层
          this.createBjCluster(1000);
          // 创建所有点位图层，在取消聚合的时候展示出来
          this.createAllPonitsLayer();
        },
        // 绑定地图事件
        bindMapEvt() {
          // 监听鼠标点击
          this.map.on('click', (evt) => {
            const coordinate = ol.proj.transform(evt.coordinate, 'EPSG:3857', 'EPSG:4326')
            console.log('当前点击坐标为 : ' + coordinate[0].toFixed(7) + ',' + coordinate[1].toFixed(7));
            const feature = this.map.forEachFeatureAtPixel(evt.pixel, function (feature) {
              return feature;
            });
            // 如果点击的是聚合点，进一步放大层级。如果点击的是具体的点位了（聚合数量是1），获取点位ID，进行下一步操作
            if (feature) {
              console.log(feature, 'feature');
              // 获取count属性
              const count = feature.get('count');
              // 放大地图层级
              if (count > 1) {
                alert(`这是一个聚合点，下面共有 ${count} 个点位，将以此为中心，增大一级地图显示层级。`);
                const czoom = this.map.getView().getZoom();
                this.map.getView().animate({
                  center: evt.coordinate,
                  zoom: czoom + 1
                });
                // 弹出ID
              } else {
                const custom = feature.get('features')[0].get('custom');
                alert(`这是一个具体的点位，点位的ID是 ${custom.id}。`);
              }
            }
          });
          window.aaa = this.map;
          // 监听鼠标移动，移动到feature上时，鼠标变为可点击的状态
          this.map.on('pointermove', (e) => {
            let pixel = this.map.getEventPixel(e.originalEvent);
            let feature = this.map.forEachFeatureAtPixel(pixel, (feature) => {
              return feature
            });
            if (feature) {
              this.map.getTargetElement().style.cursor = 'pointer';
            } else {
              this.map.getTargetElement().style.cursor = 'auto';
            }
          });
          // 移动事件，包括鼠标左键移动和缩放，地图右上角显示当前地图层级
          this.map.on('moveend', () => {
            this.mapZoom = this.map.getView().getZoom().toFixed(1);
          });
        },
        // 创建随机点位的聚合点位数据图层
        createRandomCluster(num) {
          const positions = this.createPointsByRange(num); // 生成坐标数据
          this.createCluster(positions, 'allCluster', 3);
          this.allPoints = positions;
        },
        // 创建北京的聚合数据
        createBjCluster(num) {
          const bjPoints = this.createPointsByRange(num, [39.9037, 40.1892, 115.2000, 117.4000]);
          this.createCluster(bjPoints, 'beijingCluster', 2);
          this.bjPoints = bjPoints;
        },
        // 根据数据创建聚合图层
        createCluster(points, layerID, zindex) {
          // 根据points创建一个新的数据源和要素数组，
          const vectorSource = new ol.source.Vector({
            features: points.map(e => {
              // ol.proj.fromLonLat用于将经纬度坐标从 WGS84 坐标系转换为地图投影坐标系
              const feature = new ol.Feature({
                geometry: new ol.geom.Point(ol.proj.fromLonLat(e)),
                custom: {
                  id: Math.ceil(Math.random() * 100000)
                }
              });
              return feature;
            })
          });
          // 根据点位创建聚合资源
          const clusterSource = new ol.source.Cluster({
            distance: this.currentDis, // 设置多少像素以内的点位进行聚合
            source: vectorSource,
          });
          // 创建带有数据源的矢量图层，将创建的聚合字段作为source
          this[layerID] = new ol.layer.Vector({
            source: clusterSource,
            layerID: layerID,
            style: (feature) => {
              return this.setFeatureStyle(feature); // 设置聚合点的样式
            }
          });
          // 将矢量图层添加到地图上
          this.map.addLayer(this[layerID]);
          this[layerID].setZIndex(zindex); // 设置层级
        },
        // 设置聚合点的样式
        setFeatureStyle(feature) {
          // 获取聚合点小有几个点位
          const size = feature.get('features').length;
          // 设置聚合点的count参数
          feature.set('count', size);
          // 如果是1，直接展示点位的样式
          if (size === 1) {
            return basePointStyle;
          } else {
            // 如果是聚合点，查看countStyles是否存储了这个聚合点的数字样式，如果不存在，生成一个并存储
            if (!countStyles[size]) {
              countStyles[size] = createCountPointStyle(size);
            }
            return countStyles[size];
          }
        },
        // 设置聚合图层的聚合距离
        handleClickCluster(btn) {
          // 聚合距离相同，直接返回不处理
          if (this.currentDis === btn.px) {
            return;
          }
          this.currentDis = btn.px;
          // 设置聚合距离
          // 获取到图层，遍历判断是不是 Cluster 图层，是的话设置聚合距离
          this.map.getLayers().getArray().forEach(layer => {
            const source = layer.getSource();
            if (source instanceof ol.source.Cluster) {
              !layer.getVisible() && layer.setVisible(true); // 如果聚合图层隐藏着呢，将其显示
              source.setDistance(btn.px);
            }
          });
          // 如果点位图层显示着呢，将其隐藏
          this.pointsLayers.getVisible() && this.pointsLayers.setVisible(false); // 隐藏点位图层
        },
        // 根据type取消聚合显示
        handleClickCancalCluster(type) {
          if (this.cancalClusterType === type) { // 防止二次点击不聚合
            return;
          }
          this.cancalClusterType = type;
          this.currentDis = 0;
          if (this.cancalClusterType === 1) {
            // 遍历所有图层，将聚合图层的聚合距离设置为0，使其表现为布局和的样子
            this.map.getLayers().getArray().forEach(layer => {
              const source = layer.getSource();
              if (source instanceof ol.source.Cluster) {
                source.setDistance(0); // 设置聚合距离
                layer.setVisible(true); // 将图层显示出来
              }
            });
            this.pointsLayers.setVisible(false);
            console.log('样式区别');
          } else {
            // 隐藏聚合图层
            this.map.getLayers().getArray().forEach(layer => {
              const source = layer.getSource();
              if (source instanceof ol.source.Cluster) {
                layer.setVisible(false);
              }
            });
            this.pointsLayers.setVisible(true); // 显示点位图层
            console.log('隐藏显示');
          }
        },
        // 根据范围随机生成经纬度点位 rangeArr = [minLat, maxLat, minLon, maxLon]
        createPointsByRange(num, rangeArr = [3.86, 53.56, 73.66, 135.05]) {
          const [minLat, maxLat, minLon, maxLon] = rangeArr;
          const points = [];
          for (var i = 0; i < num; i++) {
            var lat = Math.random() * (maxLat - minLat) + minLat;
            var lon = Math.random() * (maxLon - minLon) + minLon;
            points.push([lon, lat]);
          }
          return points;
        },
        // 创建所有点位图层，在取消聚合的时候展示出来
        createAllPonitsLayer() {
          const { bjPoints, allPoints } = this;
          this.pointsLayers = new ol.layer.Vector({
            source: new ol.source.Vector({
              features: [...bjPoints, ...allPoints].map(e => {
                const feature = new ol.Feature({
                  geometry: new ol.geom.Point(ol.proj.fromLonLat(e)),
                  custom: {
                    id: Math.ceil(Math.random() * 100000)
                  }
                });
                return feature;
              })
            }),
            layerID: 'appPoints',
            style: basePointStyle
          });
          this.map.addLayer(this.pointsLayers);
          this.pointsLayers.setVisible(false); // 默认隐藏点位图层
          this.pointsLayers.setZIndex(3); // 设置层级
        }
      },
      mounted() {
        this.initMap();
      }
    }).mount('#app')
  </script>
  
</body>

</html>